We’ll learn 3 things today:
We’ll first look at some usecases of shiny to movitate why we should care about this technology. Then we’ll learn the fundamentals and work through some exercises. After that, we’ll tackle reactive programming, the most important concept and technique in Shiny. And finally, we’ll wrap up with modules, a way to isolate and reuse code. Let’s get started!
Shiny is a technology that allows you to rapidly build web apps using R. You can host shiny apps either by yourself or at Shinyapps.io.
Shiny is perfect for anyone who knows R but NOT html/javascript/css and still wants to make web apps. These people include:
Interactive plots. For example, a bar chart that shows the actual values when you mouse over the bars. A timeseries plot that changes as you select different periods.
Turn your ideas into reality. For example, this app allows you to look up a user’s most recent readings on Douban (豆瓣). Notice you can also embed google ads in the app. This app provides a cleaner interface to search restaurants on Tabelog in English.
Embed data analysis in your blog post so that readers can play with the results. Example.
Super easy to run calculations under different scenarios or statistical procedures with different input parameters. For example, calculate sample sizes in multi-regional clinical trials.
Train and backtest various models, compare results and generate reports. For example, here’s an app that does all of that for survival models.
You can build an entire product/SaaS platform using Shiny. Example.
pkgs = c("shiny", "shinydashboard", "tidyverse", "plotly")
for (pkg in pkgs) {
if (!pkg %in% installed.packages()) {
cat(paste(pkg, "missing, will attempt to install\n"))
install.packages(pkg)
} else cat(paste(pkg, "installed OK\n"))
}Ok. With all that out of the way, let’s start learning shiny. First, let’s run a very boring shiny app called “hello, world!” Let me open the app’s R file (examples/00-hello-world-boring.R). We’re in Rstudio. Notice the green Run App button.
Let’s look at its code. We see it’s very simple. Indeed, the app does nothing except printing “Hello World!” to the screen. Notice we need to load the shiny package. There’re 2 parts: ui and server. The server is just a function, an empty function in this case. The ui is html code returned by fluidPage(). Finally, we pass ui and server into shinyApp() to make the app.
library(shiny)
# make UI ---
ui <- fluidPage(
titlePanel("Hello World!")
)
# make server ---
server <- function(input, output) {}
# make Shiny app ----
shinyApp(ui = ui, server = server)Let’s update the app to also introduce people to the world. To do that, we
Let’s see how it works. Run examples/01-hello-world.R.
Now let’s look at the code. Notice for the ui, I added a mainPanel(), and inside of it, I used textInput() to implement the text box where a user can input its name. And I used textOutput() to print the output on the screen. The string “my_name” is the input id and “self_intro” is the output id. Pay attention to how we use them in the server function.
# make UI ---
ui <- fluidPage(
titlePanel("Hello World!"),
mainPanel(
textInput("my_name",
label = "Enter your name:",
value = "Harry"),
textOutput("self_intro")
)
)
# make server ---
server <- function(input, output) {
output$self_intro = renderText(
paste("My name is", input$my_name)
)
}Next, let’s add a sidebar to the app. Once again, let’s see how the finish app should look like.
Run examples/01-hello-world-w-sidebar.R (May need to make the viewer pane wider to see the sidebar.)
Because we’re only changing the ui, the server code aren’t changed. Let’s read the code of the UI. You see I used a function sidebarLayout(), and inside of it I used sidebarPanel() and mainPanel(). I now put the input text box inside of sidebarPanel() and only leave the output in mainPanel(). This is a simple pattern that you may want to remember.
ui <- fluidPage(
titlePanel("Hello World!"),
sidebarLayout(
sidebarPanel(
textInput("my_name",
label = "Enter your name:",
value = "Harry")
),
mainPanel(
textOutput("self_intro")
)
)
)server <- function(input, output) {
1. takes user supplied values from input
2. run computation
3. return or display results through output
- renderText()
- renderPlot()
}Let’s make a shiny app together. The app should
Let me open exercises/01-plot-histogram.R. The server function is already completed. We need to finish the ui.
At this time, switch to R studio and work on the R file. Make sure you ask students for answers.
It’s your turn. Make a shiny app. It should
You have 4 minutes.
You have 8 minutes.
For larger apps, we can break the UI and server into separate files. We then put these smaller files in sub-folders /ui and /server. Note we still have ui.R and server.R, and they contain code that calls the those files in /ui and /server. We can also create a global.R file and put inside code that load packages, set paths, source helpers, defines options, and etc.
library(shiny)
library(shinyjs)
library(dplyr)
library(ggplot2)
# change max upload file size to 30 MB, default is 5 MB
options(shiny.maxRequestSize = 30*1024^2, shiny.reactlog = T)
# set paths
helper_path = "R/helper"
ui_path = "R/ui"
server_path = "R/server"
# load helper functions
for (fname in list.files(helper_path))
source(file.path(helper_path, fname))ui <- fluidPage(theme = "darkly.css", id = "navbar",
...
tabPanel(title = "Fake",
uiOutput("hclust_filter"),
uiOutput("hclust_num"),
actionButton("run_hclust", "Run"),
plotOutput("dendrogram", height = "600px"),
downloadButton('download_plt_hclust', "Download Plot")
)
...
)server <- function(input, output) {
# load ui related source files
source(file.path(ui_path, "ui-simulate.R"), local=T)
source(file.path(ui_path, "ui-simulate-allon-alloff-buttons.R"), local=T)
# load server related source files
source(file.path(server_path, "01-load-n-prep-data.R"), local=T)
source(file.path(server_path, "02-simulate.R"), local=T)
}If you know some css and js, you can do something fancy with shiny. Always put your logo (.png or .jpeg) and css (.css) files inside the subfolder /www.
Let’s look at an app styled with css and js.
Run examples/02-output-data-table/ui.R
We see the app looks better. This is because we used a theme. Themes are like make-ups and clothing. They make your shiny app look nice. If you know CSS, you can create your own theme. The 1st tab shows the first 15 rows of the iris data in table format, and it allows you to paginate through the data. The 2nd tab shows all data on one page. Try to search 3.5 and see what you get. The 3rd tab removes the search box and shows only the first 10 rows. The 4th tab uses javascript to change the color of a number to yellow if it’s >= 5.
Ask students to read the code themselves as homework.
Let’s start with an exercise. Open the 04-reactivity-00.R file under the /exercises folder. Your task is to complete the server function so that the app shows a simple plot of the 1st nrows of cars, a built-in data frame. Let me run the app so that you see how it works.
Run solutions/04-reactivity-00.R.
We see as we change the number of rows, the scatter plot updates accordingly. Go ahead start. You have 2 minutes.
credit: Joe Cheng
Ok. Time is up. Raise your hand if you get something like this.
output$plot <- renderPlot({
plot(head(cars, input$nrows))
})Congratulations! You’ve just implemented reactivity. Workshop is over and you all can go to dinner now. Yeah, I wish reactivity is that simple. Unfortunately, it’s not, so we’ll have to work harder. But the payoff will be huge as reactivity is the single most important piece of shiny. When you make a complex shiny app that works but works very slowly, 99% of the time it’s because you didn’t implement reactivity correctly.
Let’s break down the solution we just saw and learn some reactive terms.
input$nrows plays the role of source, and it’s implemented in R by something called Reactive value. Reactive values implement reactive sources.output$plot plays the role of endpoint, and it’s implemented in R by something called Observer, specifically, renderPlot. Observers implement reactive endpoints.output$plot <- renderPlot({
plot(head(cars, input$nrows))
})| Role | Implementation (R) | |
|---|---|---|
| input$nrows | Reactive source | Reactive value |
| output$plot | Reactive endpoint | Observer ( renderPlot({}) ) |
Let’s look at another example. This app finds the nth Fibonacci number and its inverse. I have two implementations. Both are pretty slow because they use recursion. But one is slower than the other because it didn’t implement reactivity correctly.
** SKIP WHEN NO TIME **
Let’s first run the slower version.
Run examples/03-reactivity-fibonacci-slow.R.
Let’s run the faster version.
Run examples/03-reactivity-fibonacci-fast.R.
credit: Reactivity - An overview
** SKIP WHEN NO TIME **
Let’s look at the slower version in detail. fib() is the recursive function for calculating the nth Fibonacci number. Notice we pass the source input$n into fib() twice. We also used the observer renderText({}) twice and directed the rendered text to the endpoints output$nth_fib and output$nth_fib_inv respectively.
server <- function(input, output) {
# run fib() twice
output$nth_fib <- renderText({ fib(input$n) })
output$nth_fib_inv <- renderText({ 1 / fib(input$n) })
}Let’s now look at the faster version in detail. Notice we pass the source input$n into fib() only once here. And we put fib(input$n) inside of reactive({}). reactive({}) is a reactive expression. It caches the value returned by fib() so that no matter how many times current_fib() is called, as long as input$n doesn’t change, it’ll simply look up the value instead of calling fib() to re-compute it. By the way, current_fib() plays the role of reactive conductor, connecting source and end point.
server <- function(input, output) {
# only run fib() once
current_fib <- reactive({ fib(input$n) })
output$nth_fib <- renderText({ current_fib() })
output$nth_fib_inv <- renderText({ 1 / current_fib() })
}We can now complete the table of reactive terminologies. As we just mentioned, current_fib() plays the role of conductor. It’s implemented in R by something called reactive expression, reactive({}).
server <- function(input, output) {
# only run fib() once
current_fib <- reactive({ fib(input$n) })
output$nth_fib <- renderText({ current_fib() })
output$nth_fib_inv <- renderText({ 1 / current_fib() })
}| Role | Implementation (R) | |
|---|---|---|
| input$nrows | Reactive source | Reactive value |
| current_fib() | Reactive conductor | Reactive expression ( reactive({}) ) |
| output$plot | Reactive endpoint | Observer ( renderPlot({}) ) |
Source -> Conductor1 -> Conductor2 … -> Endpoint
Here’re some examples of reactive expressions and observers. A reactive expression always returns a value. An observer never returns a value, instead, it performs side effects. This is a key difference between reactive expressions and observers. Think of the difference between pure functions and functions with side effects only such as print().
| Reactive Expression | Observer | |
|---|---|---|
| R code | reactive({}) |
observe({}) |
renderText({}) |
||
renderPlot({}) |
||
renderDataTable({}) |
||
| … |
Ok. Let’s do an exercise.
Open exercises/04-reactivity-01.R
Your task is to
Re-write the server logic to ensure head() is only run once for every change to input$nrows.
credit: Joe Cheng
How many of you get something like this? Any of you did something different?
server <- function(input, output) {
df <- reactive({
head(cars, input$nrows)
})
output$plot <- renderPlot({
plot(df())
})
output$table <- renderTable({
df()
})
}So far, we’ve only scratched the surface. Let’s dive deeper to really understand how reactivity works. Let’s start with a very basic app.
Run examples/04-reactivity-show-number.R.
Here’s the server logic. We see it just prints the input number on the screen. So how does it work under the hood?
server <- function(input, output) {
output$text <- renderText({
print(input$a)
})
}print()First, let’s look at the following R code. We give the variable a a value of 50 and then run print(a). Afterwards, we change a to 75. This makes the expression print(a) out of date. To update it (a fancier way of saying “print a again”), we just re-run print(a).
a = 50
print(a)
a = 75 # this action makes print(a) "out of date"
# to update "print(a)", just re-run it
print(a)In general, an expression is called out of date if one or more objects in the expression has been given a new value since the expression was last called. To update an out-of-date expression is easy, just re-run the expression. This is just standard R stuff, not reactivity.
So in theory, we don’t need shiny. We can build the app from ground zero by continuously re-running every expression in the app. Information are pulled from the input instead of pushed to the output. For example, print(input$a) learns the new value of input$a because the server re-runs print(input$a), not because the new value is magically pushed into output.
In practice, this approach will quickly slow down your app and eventually make it unresposive.
Shiny solves this problem by creating a system of alerts that lets the server know when an expression becomes out of date. The server checks in on your app every few microseconds, but instead of re-running every expression, it only runs the out-of-date ones. If no alerts have appeared, the server just rests until the next check.
Alerts are like carrier pigeons.
credit: Rstudio
credit: Rstudio
credit: Rstudio
Let’s do another exercise.
Open exercises/04-reactivity-02.R
Run it and it works. The problem is that each of the 4 outputs contains copied-and-pasted logic for selecting the chosen variables, and for building the model. Can you refactor the code so it’s more efficient and maintainable?
credit: Joe Cheng
How many of you get something like this? Any of you did something different?
selected <- reactive({
iris[, c(input$xcol, input$ycol)]
})
model <- reactive({
lm(paste(input$ycol, "~", input$xcol), selected())
})Let’s look at the solution. It shows how we can chain reactive conductors. Notice selected() is a conductor (implemented by a reactive expression). And we used selected() inside another reactive expression.
We’ve seen how reactivity works. What if you want to prevent it? For example, let’s take a look at our hello-world example again.
Run examples/03-reactivity-fibonacci-slow.R.
As you can see, no matter what I put in the box, the app doesn’t show it anymore. This is accomplished by isolate(). You simply wrap isolate() around input$my_name, and then the app will stop reacting to users’ actions.
output$self_intro = renderText({
paste("My name is", isolate(input$my_name))
})The entire point of preventing reactivity is to give users more control, for example, we can make a button and when a user clicks it, the app will react.
Run examples/05-reactivity-w-button.R.
Notice I used actionButton() in the UI and gave it an id of "btn". I then pass it (input$btn) as the first argument into observeEvent(). Also notice I used isolate() around input$bins to prevent the app automatically update when user changes bins but not yet clicked on the button.
ui <- fluidPage(
sidebarPanel( actionButton("btn", "Plot") )
)
server <- function(input, output) {
observeEvent(input$btn, {
output$hist <- renderPlot({
bins <- seq(min(x), max(x),
length.out = isolate(input$bins) + 1)
hist(x, breaks = bins, col = "#75AADB", border = "white")
})
})
}A function is a bag of code. It takes some inputs and
print(), orBy using functions, you’ll isolate code and that makes it easier to read and reason about your codebase, and produce fewer errors. You can also re-use your function and hence improve your productivity.
A shiny module is a collection of 2 functions:
Switch to Rstudio.
Modulize the app: examples/06-modules-slider.R.
But it’s a pain to create unique ids yourself. Introduce NS().
NS() adds a prefix to a string.
ns <- NS("hello")
ns("world")
# > [1] "hello-world"
ns("friend")
# > [1] "hello-friend"We can use NS() to create ids.
slider_ui <- function(id) {
# id: string
ns <- NS(id)
tagList(
sliderInput(ns("slider"), "Slide me", 0, 100, 1),
textOutput(ns("num"))
)
} ui <- fluidPage( slider_ui("a") )
server <- function(input, output) {}
shinyApp(ui, server)Handles the server logic for the module. Must also use session.
slider <- function(input, output, session) {
output$num <- renderText({ input$slider })
}
slider_ui <- function(id) {
ns <- NS(id)
tagList(
sliderInput(ns("slider"), "Slide me", 0, 100, 1),
textOutput(ns("num"))
)
} ui <- fluidPage( slider_ui("a") )
server <- function(input, output) {
callModule(slider, "a")
}
shinyApp(ui, server)NS() to add 2 slidersSwitch to Rstudio.
Keep working on examples/06-modules-slider.R.
NS() inside module ui functionUse module to re-implement the hello-world example: exercises/05-module-hello-world.R.
You can now easily add multiple input boxes to your app. Try to add 3 of them.
Put both the module UI and server functions inside one R script.